import {Marshaller} from "./Marshaller";
import {BinarySet} from "./BinarySet";
import {NumberValue} from "./NumberValue";
import {NumberValueSet} from "./NumberValueSet";

describe('Marshaller', () => {
    describe('#marshallItem', () => {
        it('should convert objects to the DynamoDB item format', () => {
            const marshaller = new Marshaller();
            const marshalled = marshaller.marshallItem({
                string: 'foo',
                list: ['fizz', 'buzz', 'pop'],
                map: {
                    nestedMap: {
                        key: 'value',
                    }
                },
                number: 123,
                nullValue: null,
                boolValue: true,
                stringSet: new Set(['foo', 'bar', 'baz'])
            });

            expect(marshalled).toEqual({
                string: {S: 'foo'},
                list: {L: [{S: 'fizz'}, {S: 'buzz'}, {S: 'pop'}]},
                map: {
                    M: {
                        nestedMap: {
                            M: {
                                key: {S: 'value'}
                            }
                        }
                    }
                },
                number: {N: '123'},
                nullValue: {NULL: true},
                boolValue: {BOOL: true},
                stringSet: {SS: ['foo', 'bar', 'baz']}
            });
        });

        it(
            'should return an empty attribute map when provided invalid input and the onInvalid option is set to "omit"',
            () => {
                const marshaller = new Marshaller({onInvalid: 'omit'});
                expect(marshaller.marshallItem('string' as any)).toEqual({});
            }
        );

        it('should throw when provided invalid input and the onInvalid option is set to "throw"', () => {
            const marshaller = new Marshaller({onInvalid: 'throw'});
            expect(() => marshaller.marshallItem('string' as any)).toThrow();
        });
    });

    describe('#marshallValue', () => {
        describe('strings', () => {
            it('should convert strings to StringAttributeValues', () => {
                expect((new Marshaller()).marshallValue('string'))
                    .toEqual({S: 'string'});
            });

            it(
                'should convert empty strings to null when onEmpty option set to "nullify"',
                () => {
                    expect(
                        (new Marshaller({onEmpty: 'nullify'})).marshallValue('')
                    ).toEqual({NULL: true});
                }
            );

            it(
                'should remove empty strings when onEmpty option set to "omit"',
                () => {
                    expect(
                        (new Marshaller({onEmpty: 'omit'})).marshallValue('')
                    ).toBeUndefined();
                }
            );

            it(
                'should convert empty strings to StringAttributeValues otherwise',
                () => {
                    expect((new Marshaller()).marshallValue(''))
                        .toEqual({S: ''});
                }
            );
        });

        describe('binary values', () => {
            it('should convert binary values to BinaryAttributeValues', () => {
                const bin = Uint8Array.from([0xde, 0xad, 0xbe, 0xef]);
                expect((new Marshaller()).marshallValue(bin))
                    .toEqual({B: bin});
            });

            it(
                'should convert empty binary values to null when onEmpty option set to "nullify"',
                () => {
                    expect(
                        (new Marshaller({onEmpty: 'nullify'}))
                            .marshallValue(new Uint8Array(0))
                    ).toEqual({NULL: true});
                }
            );

            it(
                'should omit empty binary values when onEmpty option set to "omit"',
                () => {
                    expect(
                        (new Marshaller({onEmpty: 'omit'}))
                            .marshallValue(new Uint8Array(0))
                    ).toBeUndefined();
                }
            );

            it(
                'should convert empty binary values to null when onEmpty option set to "nullify"',
                () => {
                    expect(
                        (new Marshaller()).marshallValue(new Uint8Array(0))
                    ).toEqual({B: new Uint8Array(0)});
                }
            );
        });

        describe('numbers', () => {
            it('should convert numbers to NumberAttributeValues', () => {
                expect((new Marshaller()).marshallValue(42))
                    .toEqual({N: '42'});
            });

            it('should convert NumberValues to NumberAttributeValues', () => {
                expect(
                    (new Marshaller()).marshallValue(new NumberValue('123'))
                ).toEqual({N: '123'});
            });
        });

        describe('null', () => {
            it('should convert nulls to NullAttributeValues', () => {
                expect((new Marshaller()).marshallValue(null))
                    .toEqual({NULL: true});
            });
        });

        describe('boolean', () => {
            it('should convert booleans to BooleanAttributeValues', () => {
                const marshaller = new Marshaller();
                expect(marshaller.marshallValue(true)).toEqual({BOOL: true});
                expect(marshaller.marshallValue(false)).toEqual({BOOL: false});
            });
        });

        describe('lists', () => {
            it('should convert arrays to ListAttributeValues', () => {
                expect((new Marshaller()).marshallValue([])).toEqual({L: []});
            });

            it('should convert list members to AttributeValues', function() {
                expect(
                    (new Marshaller()).marshallValue(['a', 1, true, null, {}])
                ).toEqual({L: [
                    {S: 'a'},
                    {N: '1'},
                    {BOOL: true},
                    {NULL: true},
                    {M: {}},
                ]});
            });

            it('should convert iterables to ListAttributeValues', () => {
                const inputGen = function *() {
                    yield 'a';
                    yield 1;
                    yield true;
                    yield null;
                    yield {};
                };

                expect(
                    (new Marshaller()).marshallValue(inputGen())
                ).toEqual({L: [
                    {S: 'a'},
                    {N: '1'},
                    {BOOL: true},
                    {NULL: true},
                    {M: {}},
                ]});
            });

            it('should omit undefined values from the serialized list', () => {
                expect(
                    (new Marshaller())
                        .marshallValue([
                            'a',
                            undefined,
                            1,
                            undefined,
                            true,
                            undefined,
                            null,
                            undefined,
                            {}
                        ])
                ).toEqual({L: [
                    {S: 'a'},
                    {N: '1'},
                    {BOOL: true},
                    {NULL: true},
                    {M: {}},
                ]});
            });
        });

        describe('maps', () => {
            it('should convert objects to MapAttributeValues', () => {
                expect((new Marshaller()).marshallValue({})).toEqual({M: {}});
            });

            it('should convert maps to MapAttributeValues', () => {
                expect((new Marshaller()).marshallValue(new Map()))
                    .toEqual({M: {}});
            });

            it(
                'should omit keys whose values are serialized as undefined',
                () => {
                    const marshaller = new Marshaller();
                    expect(marshaller.marshallValue({a: void 0}))
                        .toEqual({M: {}});

                    expect(marshaller.marshallValue(new Map([['a', void 0]])))
                        .toEqual({M: {}});
                }
            );

            it(
                'should convert objects with inheritance chains to MapAttributeValues',
                () => {
                    class MyPrototype {
                        public readonly foo: string = 'bar';
                    }

                    class MyDescendant extends MyPrototype {
                        public readonly fizz: string = 'buzz';
                    }

                    const myInstance = new MyDescendant();
                    (myInstance as any).quux = true;

                    expect((new Marshaller()).marshallValue(myInstance))
                        .toEqual({
                            M: {
                                foo: {S: 'bar'},
                                fizz: {S: 'buzz'},
                                quux: {BOOL: true}
                            }
                        });
                }
            );

            it('should convert map members to AttributeValues', () => {
                const map = new Map<string, any>();
                map.set('a', 'a');
                map.set('b', 1);
                map.set('c', true);
                map.set('d', null);
                map.set('e', ['s']);

                expect((new Marshaller()).marshallValue(map)).toEqual({
                    M: {
                        a: {S: 'a'},
                        b: {N: '1'},
                        c: {BOOL: true},
                        d: {NULL: true},
                        e: {L: [{S: 's'}]}
                    }
                });
            });

            it(
                'should omit map members whose keys are not strings when the onInvalid option is "omit"',
                () => {
                    const marshaller = new Marshaller({
                        onInvalid: 'omit'
                    });
                    const map = new Map<any, any>();
                    map.set('a', 'a');
                    map.set(1, 1);
                    map.set({}, true);
                    map.set([], null);
                    map.set(null, ['s']);

                    expect(marshaller.marshallValue(map))
                        .toEqual({M: {a: {S: 'a'}}});
                }
            );

            it('should throw otherwise', () => {
                const marshaller = new Marshaller();
                const map = new Map<any, any>();
                map.set('a', 'a');
                map.set(1, 1);
                map.set({}, true);
                map.set([], null);
                map.set(null, ['s']);

                expect(() => marshaller.marshallValue(map)).toThrow();
            });
        });

        describe('sets', () => {
            it(
                'should omit empty sets when the onEmpty option is "omit"',
                () => {
                    const marshaller = new Marshaller({onEmpty: 'omit'});
                    expect(marshaller.marshallValue(new Set()))
                        .toBeUndefined();
                }
            );

            it(
                'should convert empty sets to null when the onEmpty option is "nullify"',
                () => {
                    const marshaller = new Marshaller({onEmpty: 'nullify'});
                    expect(marshaller.marshallValue(new Set()))
                        .toEqual({NULL: true});
                }
            );
            it(
                'should omit empty sets when the onEmpty option is "leave", as the kind of set cannot be inferred',
                () => {
                    const marshaller = new Marshaller({onEmpty: 'leave'});
                    expect(marshaller.marshallValue(new Set()))
                        .toBeUndefined();
                }
            );

            it(
                'should omit sets with members of an unknown type when the onEmpty option is "omit"',
                () => {
                    const marshaller = new Marshaller({
                        onInvalid: 'omit'
                    });
                    const set = new Set<object>();
                    set.add({});
                    expect(marshaller.marshallValue(set))
                        .toBeUndefined();
                }
            );

            it(
                'should throw on sets with members of an unknown type otherwise',
                () => {
                    const marshaller = new Marshaller();
                    const set = new Set<object>();
                    set.add({});
                    expect(() => marshaller.marshallValue(set)).toThrow();
                }
            );

            it(
                'should drop invalid members when onInvalid option is set to "omit"',
                () => {
                    const marshaller = new Marshaller({
                        onInvalid: 'omit'
                    });
                    expect(marshaller.marshallValue(new Set(['a', 1, 'c'])))
                        .toEqual({SS: ['a', 'c']});
                }
            );

            it('should throw on invalid members otherwise', () => {
                const marshaller = new Marshaller();
                expect(
                    () => marshaller.marshallValue(new Set(['a', 1, 'c']))
                ).toThrow();
            });

            it(
                'should return a NullAttributeValue for an emptied set when onEmpty is set to "nullify"',
                () => {
                    const marshaller = new Marshaller({onEmpty: 'nullify'});
                    expect(marshaller.marshallValue(new Set([''])))
                        .toEqual({NULL: true});
                }
            );

            it(
                'should return undefined for an emptied set when onEmpty is set to "omit"',
                () => {
                    const marshaller = new Marshaller({onEmpty: 'omit'});
                    expect(marshaller.marshallValue(new Set([''])))
                        .toBeUndefined();
                }
            );

            it('should serialize empty values otherwise', () => {
                const marshaller = new Marshaller();
                expect(marshaller.marshallValue(new Set([''])))
                    .toEqual({SS: ['']});
            });

            describe('string sets', () => {
                it(
                    'should convert sets with strings into StringSetAttributeValues',
                    () => {
                        expect(
                            (new Marshaller())
                                .marshallValue(new Set<string>(['a', 'b', 'c']))
                        ).toEqual({SS: ['a', 'b', 'c']});
                    }
                );

                it(
                    'should drop empty members when onEmpty option is set to "nullify"',
                    () => {
                        expect(
                            (new Marshaller({onEmpty: 'nullify'}))
                                .marshallValue(new Set<string>(['a', '', 'c']))
                        ).toEqual({SS: ['a', 'c']});
                    }
                );

                it(
                    'should drop empty members when onEmpty option is set to "omit"',
                    () => {
                        expect(
                            (new Marshaller({onEmpty: 'omit'}))
                                .marshallValue(new Set<string>(['a', '', 'c']))
                        ).toEqual({SS: ['a', 'c']});
                    }
                );

                it('should keep empty members otherwise', () => {
                    expect(
                        (new Marshaller())
                            .marshallValue(new Set<string>(['a', '', 'c']))
                    ).toEqual({SS: ['a', '', 'c']});
                });
            });

            describe('number sets', () => {
                it(
                    'should convert sets with numbers into NumberSetAttributeValues',
                    () => {
                        expect(
                            (new Marshaller())
                                .marshallValue(new Set<number>([1, 2, 3]))
                        ).toEqual({NS: ['1', '2', '3']});
                    }
                );

                it(
                    'should convert NumberValueSet objects into NumberSetAttributeValues',
                    () => {
                        expect(
                            (new Marshaller())
                                .marshallValue(new NumberValueSet([
                                    new NumberValue('1'),
                                    new NumberValue('2'),
                                    new NumberValue('3'),
                                ]))
                        ).toEqual({NS: ['1', '2', '3']});
                    }
                );
            });

            describe('binary sets', () => {
                it(
                    'should convert sets with binary values into BinarySetAttributeValues',
                    () => {
                        const marshaller = new Marshaller();
                        const converted = marshaller.marshallValue(new BinarySet([
                            Uint8Array.from([0xde, 0xad]),
                            Uint8Array.from([0xbe, 0xef]).buffer,
                            Uint8Array.from([0xfa, 0xce]),
                        ]));
                        expect(converted).toEqual({BS: [
                            Uint8Array.from([0xde, 0xad]),
                            Uint8Array.from([0xbe, 0xef]).buffer,
                            Uint8Array.from([0xfa, 0xce]),
                        ]});
                    }
                );

                it(
                    'should drop empty members when the onEmpty option is set to "nullify"',
                    () => {
                        const marshaller = new Marshaller({onEmpty: 'nullify'});
                        const converted = marshaller.marshallValue(new BinarySet([
                            Uint8Array.from([0xde, 0xad]),
                            Uint8Array.from([0xbe, 0xef]).buffer,
                            Uint8Array.from([0xfa, 0xce]),
                            new Uint8Array(0),
                        ]));
                        expect(converted).toEqual({BS: [
                            Uint8Array.from([0xde, 0xad]),
                            Uint8Array.from([0xbe, 0xef]).buffer,
                            Uint8Array.from([0xfa, 0xce]),
                        ]});
                    }
                );

                it(
                    'should drop empty members when the onEmpty option is set to "omit"',
                    () => {
                        const marshaller = new Marshaller({onEmpty: 'omit'});
                        const converted = marshaller.marshallValue(new BinarySet([
                            Uint8Array.from([0xde, 0xad]),
                            Uint8Array.from([0xbe, 0xef]).buffer,
                            Uint8Array.from([0xfa, 0xce]),
                            new Uint8Array(0),
                        ]));
                        expect(converted).toEqual({BS: [
                            Uint8Array.from([0xde, 0xad]),
                            Uint8Array.from([0xbe, 0xef]).buffer,
                            Uint8Array.from([0xfa, 0xce]),
                        ]});
                    }
                );

                it('should keep empty members otherwise', () => {
                    const marshaller = new Marshaller();
                    const converted = marshaller.marshallValue(new BinarySet([
                        Uint8Array.from([0xde, 0xad]),
                        Uint8Array.from([0xbe, 0xef]).buffer,
                        Uint8Array.from([0xfa, 0xce]),
                        new Uint8Array(0),
                    ]));
                    expect(converted).toEqual({BS: [
                        Uint8Array.from([0xde, 0xad]),
                        Uint8Array.from([0xbe, 0xef]).buffer,
                        Uint8Array.from([0xfa, 0xce]),
                        new Uint8Array(0),
                    ]});
                });
            });
        });

        describe('undefined values', () => {
            it('should return undefined for undefined', () => {
                expect((new Marshaller().marshallValue(void 0)))
                    .toBeUndefined();
            });
        });

        describe('symbols', () => {
            it(
                'should omit symbols when the onInvalid option is set to "omit"',
                () => {
                    expect(
                        (new Marshaller({onInvalid: 'omit'})
                            .marshallValue(Symbol.iterator))
                    ).toBeUndefined();
                }
            );

            it('should throw on symbols otherwise', () => {
                expect(
                    () => (new Marshaller().marshallValue(Symbol.iterator))
                ).toThrow();
            });
        });

        describe('functions', () => {
            it(
                'should omit functions when the onInvalid option is set to "omit"',
                () => {
                    expect(
                        (new Marshaller({onInvalid: 'omit'})
                            .marshallValue(() => {}))
                    ).toBeUndefined();
                }
            );

            it('should throw on symbols otherwise', () => {
                expect(
                    () => (new Marshaller().marshallValue(() => {}))
                ).toThrow();
            });
        });
    });

    describe('#unmarshallItem', () => {
        it('should convert DynamoDB items to plain vanilla JS objects', function() {
            var unmarshalled = (new Marshaller({unwrapNumbers: true})).unmarshallItem({
                string: {S: 'foo'},
                list: {L: [{S: 'fizz'}, {S: 'buzz'}, {S: 'pop'}]},
                map: {
                    M: {
                        nestedMap: {
                            M: {
                                key: {S: 'value'}
                            }
                        }
                    }
                },
                number: {N: '123'},
                nullValue: {NULL: true},
                boolValue: {BOOL: true}
            });

            expect(unmarshalled).toEqual({
                string: 'foo',
                list: ['fizz', 'buzz', 'pop'],
                map: {
                    nestedMap: {
                        key: 'value',
                    }
                },
                number: 123,
                nullValue: null,
                boolValue: true
            });
        });
    });

    describe('#unmarshallValue', () => {
        const marshaller = new Marshaller();
        describe('strings', () => {
            it('should convert StringAttributeValues to strings', () => {
                expect(marshaller.unmarshallValue({S: 'string'}))
                    .toEqual('string');
            });
        });

        describe('binary values', () => {
            it('should convert BinaryAttributeValues to binary values', () => {
                expect(marshaller.unmarshallValue({B: new Uint8Array(1)}))
                    .toEqual(new Uint8Array(1));
            });
        });

        describe('numbers', () => {
            it(
                'should convert NumberAttributeValues to NumberValues',
                () => {
                    const unsafeInteger = '9007199254740991000';
                    const converted = marshaller.unmarshallValue({N: unsafeInteger}) as NumberValue;
                    expect(converted.toString()).toBe(unsafeInteger);
                }
            );

            it(
                'should convert NumberAttributeValues to numbers when unwrapNumbers is true',
                () => {
                    const marshaller = new Marshaller({unwrapNumbers: true});
                    expect(marshaller.unmarshallValue({N: '42'})).toEqual(42);
                }
            );
        });

        describe('null', () => {
            it('should convert NullAttributeValues to null', () => {
                expect(marshaller.unmarshallValue({NULL: true})).toEqual(null);
            });
        });

        describe('boolean', () => {
            it('should convert BooleanAttributeValues to booleans', () => {
                expect(marshaller.unmarshallValue({BOOL: true})).toEqual(true);
                expect(marshaller.unmarshallValue({BOOL: false}))
                    .toEqual(false);
            });
        });

        describe('lists', () => {
            it('should convert ListAttributeValues to lists', () => {
                expect(marshaller.unmarshallValue({L: []})).toEqual([]);
            });

            it('should convert member AttributeValues to list members', () => {
                expect(marshaller.unmarshallValue({L: [
                    {S: 'a'},
                    {N: '1'},
                    {BOOL: true},
                    {NULL: true},
                    {M: {}}
                ]})).toEqual(['a', new NumberValue('1'), true, null, {}]);
            });
        });

        describe('maps', () => {
            it('should convert MapAttributeValues to objects', () => {
                expect(marshaller.unmarshallValue({M: {}})).toEqual({});
            });

            it('should convert member AttributeValues to map members', () => {
                expect(marshaller.unmarshallValue({
                    M: {
                        a: {S: 'a'},
                        b: {N: '1'},
                        c: {BOOL: true},
                        d: {NULL: true},
                        e: {L: [{S: 's'}]}
                    }
                })).toEqual({
                    a: 'a',
                    b: new NumberValue('1'),
                    c: true,
                    d: null,
                    e: ['s'],
                });
            });
        });

        describe('string sets', () => {
            it(
                'should convert StringSetAttributeValues into sets with strings',
                () => {
                    expect(marshaller.unmarshallValue({SS: ['a', 'b', 'c']}))
                        .toEqual(new Set(['a', 'b', 'c']));
                }
            );
        });

        describe('number sets', () => {
            it(
                'should convert NumberSetAttributeValues into sets with NumberValues',
                function() {
                    const unsafeInteger = '900719925474099100';
                    const converted = marshaller.unmarshallValue({NS: [
                        unsafeInteger + '1',
                        unsafeInteger + '2',
                        unsafeInteger + '3',
                    ]});

                    expect(converted).toEqual(new NumberValueSet([
                        new NumberValue(unsafeInteger + '1'),
                        new NumberValue(unsafeInteger + '2'),
                        new NumberValue(unsafeInteger + '3'),
                    ]));
                }
            );

            it(
                'should convert NumberSetAttributeValues into sets with numbers when unwrapNumbers is true',
                () => {
                    const marshaller = new Marshaller({unwrapNumbers: true});
                    expect(marshaller.unmarshallValue({NS: ['1', '2', '3']}))
                        .toEqual(new Set([1, 2, 3]));
                }
            );
        });

        describe('binary sets', () => {
            it(
                'should convert BinarySetAttributeValues into sets with binary strings',
                () => {
                    expect(
                        marshaller.unmarshallValue({BS: [
                            new Uint8Array(1),
                            new Uint8Array(2),
                        ]})
                    ).toEqual(new BinarySet([
                        new Uint8Array(1),
                        new Uint8Array(2),
                    ]));
                }
            );
        });

        it('should convert objects with no values to empty maps', () => {
            expect(marshaller.unmarshallValue({foo: 'bar'} as any))
                .toEqual({});
        });
    });
});
